<!DOCTYPE html>
<html lang="en">


<head>

    <meta charset="UTF-8" />

    <title>3D 过马路游戏</title>

    <style>
        @import url("https://fonts.googleapis.com/css?family=Press+Start+2P");

        body {
            margin: 0;
            display: flex;
            font-family: "Press Start 2P", cursive;
        }

        #controls {
            position: absolute;
            bottom: 20px;
            min-width: 100%;
            display: flex;
            align-items: flex-end;
            justify-content: center;
        }

        #controls div {
            display: grid;
            grid-template-columns: 50px 50px 50px;
            gap: 10px;
        }

        #controls button {
            width: 100%;
            height: 40px;
            background-color: white;
            border: 1px solid lightgray;
            box-shadow: 3px 5px 0px 0px rgba(0, 0, 0, 0.75);
            cursor: pointer;
            outline: none;
        }

        #controls button:first-of-type {
            grid-column: 1/-1;
        }

        #score {
            position: absolute;
            top: 20px;
            left: 20px;
            font-size: 2em;
            color: white;
        }

        #result-container {
            position: absolute;
            min-width: 100%;
            min-height: 100%;
            top: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            visibility: hidden;

            #result {
                display: flex;
                flex-direction: column;
                align-items: center;
                background-color: white;
                padding: 20px;
            }

            button {
                background-color: red;
                padding: 20px 50px 20px 50px;
                font-family: inherit;
                font-size: inherit;
                cursor: pointer;
            }
        }

        #youtube,
        #youtube-card {
            display: none;
            font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
            color: black;
        }

        @media (min-height: 425px) {
            #youtube {
                z-index: 50;
                width: 100px;
                display: block;
                height: 70px;
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: red;
                border-radius: 50% / 11%;
                transform: scale(0.8);
                transition: transform 0.5s;
            }

            #youtube:hover,
            #youtube:focus {
                transform: scale(0.9);
                color: black;
            }

            #youtube::before {
                content: "";
                display: block;
                position: absolute;
                top: 7.5%;
                left: -6%;
                width: 112%;
                height: 85%;
                background: red;
                border-radius: 9% / 50%;
            }

            #youtube::after {
                content: "";
                display: block;
                position: absolute;
                top: 20px;
                left: 40px;
                width: 45px;
                height: 30px;
                border: 15px solid transparent;
                box-sizing: border-box;
                border-left: 30px solid white;
            }

            #youtube span {
                font-size: 0;
                position: absolute;
                width: 0;
                height: 0;
                overflow: hidden;
            }

            #youtube:hover+#youtube-card {
                z-index: 49;
                display: block;
                position: fixed;
                bottom: 12px;
                right: 10px;
                padding: 25px 130px 25px 25px;
                width: 300px;
                background-color: white;
            }
        }
    </style>

</head>


<body>
    <canvas class="game"></canvas>
    <div id="controls">
        <div>
            <button id="forward">▲</button><button id="left">◀</button><button id="backward">▼</button><button
                id="right">▶</button>
        </div>
    </div>
    <div id="score">0</div>
    <div id="result-container">
        <div id="result">
            <h1>Game Over</h1>
            <p>Your score:<span id="final-score"></span></p>
            <button id="retry">Retry</button>
        </div>
    </div>

    <script type="module">
        import * as THREE from "https://esm.sh/three";
        const minTileIndex = -8;
        const maxTileIndex = 8;
        const tilesPerRow = maxTileIndex - minTileIndex + 1;
        const tileSize = 42;
        function Camera() {
            const size = 300;
            const viewRatio = window.innerWidth / window.innerHeight;
            const width = viewRatio < 1 ? size : size * viewRatio;
            const height = viewRatio < 1 ? size / viewRatio : size;
            const camera = new THREE.OrthographicCamera(
                width / -2,
                width / 2,
                height / 2,
                height / -2,
                100,
                900
            );
            camera.up.set(0, 0, 1);
            camera.position.set(300, -300, 300);
            camera.lookAt(0, 0, 0);
            return camera;
        }
        function Texture(width, height, rects) {
            const canvas = document.createElement("canvas");
            canvas.width = width;
            canvas.height = height;
            const context = canvas.getContext("2d");
            context.fillStyle = "#ffffff";
            context.fillRect(0, 0, width, height);
            context.fillStyle = "rgba(0,0,0,0.6)";
            rects.forEach((rect) => {
                context.fillRect(rect.x, rect.y, rect.w, rect.h);
            });
            return new THREE.CanvasTexture(canvas);
        }
        const carFrontTexture = new Texture(40, 80, [
            { x: 0, y: 10, w: 30, h: 60 },
        ]);
        const carBackTexture = new Texture(40, 80, [
            { x: 10, y: 10, w: 30, h: 60 },
        ]);
        const carRightSideTexture = new Texture(110, 40, [
            { x: 10, y: 0, w: 50, h: 30 },
            { x: 70, y: 0, w: 30, h: 30 },
        ]);
        const carLeftSideTexture = new Texture(110, 40, [
            { x: 10, y: 10, w: 50, h: 30 },
            { x: 70, y: 10, w: 30, h: 30 },
        ]);
        export const truckFrontTexture = Texture(30, 30, [
            { x: 5, y: 0, w: 10, h: 30 },
        ]);
        export const truckRightSideTexture = Texture(25, 30, [
            { x: 15, y: 5, w: 10, h: 10 },
        ]);
        export const truckLeftSideTexture = Texture(25, 30, [
            { x: 15, y: 15, w: 10, h: 10 },
        ]);
        function Car(initialTileIndex, direction, color) {
            const car = new THREE.Group();
            car.position.x = initialTileIndex * tileSize;
            if (!direction) car.rotation.z = Math.PI;
            const main = new THREE.Mesh(
                new THREE.BoxGeometry(60, 30, 15),
                new THREE.MeshLambertMaterial({ color, flatShading: true })
            );
            main.position.z = 12;
            main.castShadow = true;
            main.receiveShadow = true;
            car.add(main);
            const cabin = new THREE.Mesh(new THREE.BoxGeometry(33, 24, 12), [
                new THREE.MeshPhongMaterial({
                    color: 0xcccccc,
                    flatShading: true,
                    map: carBackTexture,
                }),
                new THREE.MeshPhongMaterial({
                    color: 0xcccccc,
                    flatShading: true,
                    map: carFrontTexture,
                }),
                new THREE.MeshPhongMaterial({
                    color: 0xcccccc,
                    flatShading: true,
                    map: carRightSideTexture,
                }),
                new THREE.MeshPhongMaterial({
                    color: 0xcccccc,
                    flatShading: true,
                    map: carLeftSideTexture,
                }),
                new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true }),
                new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true }),
            ]);
            cabin.position.x = -6;
            cabin.position.z = 25.5;
            cabin.castShadow = true;
            cabin.receiveShadow = true;
            car.add(cabin);
            const frontWheel = Wheel(18);
            car.add(frontWheel);
            const backWheel = Wheel(-18);
            car.add(backWheel);
            return car;
        }
        function DirectionalLight() {
            const dirLight = new THREE.DirectionalLight();
            dirLight.position.set(-100, -100, 200);
            dirLight.up.set(0, 0, 1);
            dirLight.castShadow = true;
            dirLight.shadow.mapSize.width = 2048;
            dirLight.shadow.mapSize.height = 2048;
            dirLight.shadow.camera.up.set(0, 0, 1);
            dirLight.shadow.camera.left = -400;
            dirLight.shadow.camera.right = 400;
            dirLight.shadow.camera.top = 400;
            dirLight.shadow.camera.bottom = -400;
            dirLight.shadow.camera.near = 50;
            dirLight.shadow.camera.far = 400;
            return dirLight;
        }
        function Grass(rowIndex) {
            const grass = new THREE.Group();
            grass.position.y = rowIndex * tileSize;
            const createSection = (color) =>
                new THREE.Mesh(
                    new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3),
                    new THREE.MeshLambertMaterial({ color })
                );
            const middle = createSection(0xbaf455);
            middle.receiveShadow = true;
            grass.add(middle);
            const left = createSection(0x99c846);
            left.position.x = -tilesPerRow * tileSize;
            grass.add(left);
            const right = createSection(0x99c846);
            right.position.x = tilesPerRow * tileSize;
            grass.add(right);
            return grass;
        }
        const metadata = [];
        const map = new THREE.Group();
        function initializeMap() {
            metadata.length = 0;
            map.remove(...map.children);
            for (let rowIndex = 0; rowIndex > -10; rowIndex--) {
                const grass = Grass(rowIndex);
                map.add(grass);
            }
            addRows();
        }
        function addRows() {
            const newMetadata = generateRows(20);
            const startIndex = metadata.length;
            metadata.push(...newMetadata);
            newMetadata.forEach((rowData, index) => {
                const rowIndex = startIndex + index + 1;
                if (rowData.type === "forest") {
                    const row = Grass(rowIndex);
                    rowData.trees.forEach(({ tileIndex, height }) => {
                        const three = Tree(tileIndex, height);
                        row.add(three);
                    });
                    map.add(row);
                }
                if (rowData.type === "car") {
                    const row = Road(rowIndex);
                    rowData.vehicles.forEach((vehicle) => {
                        const car = Car(
                            vehicle.initialTileIndex,
                            rowData.direction,
                            vehicle.color
                        );
                        vehicle.ref = car;
                        row.add(car);
                    });
                    map.add(row);
                }
                if (rowData.type === "truck") {
                    const row = Road(rowIndex);
                    rowData.vehicles.forEach((vehicle) => {
                        const truck = Truck(
                            vehicle.initialTileIndex,
                            rowData.direction,
                            vehicle.color
                        );
                        vehicle.ref = truck;
                        row.add(truck);
                    });
                    map.add(row);
                }
            });
        }
        const player = Player();
        function Player() {
            const player = new THREE.Group();
            const body = new THREE.Mesh(
                new THREE.BoxGeometry(15, 15, 20),
                new THREE.MeshLambertMaterial({ color: "white", flatShading: true })
            );
            body.position.z = 10;
            body.castShadow = true;
            body.receiveShadow = true;
            player.add(body);
            const cap = new THREE.Mesh(
                new THREE.BoxGeometry(2, 4, 2),
                new THREE.MeshLambertMaterial({ color: 0xf0619a, flatShading: true })
            );
            cap.position.z = 21;
            cap.castShadow = true;
            cap.receiveShadow = true;
            player.add(cap);
            const playerContainer = new THREE.Group();
            playerContainer.add(player);
            return playerContainer;
        }
        const position = { currentRow: 0, currentTile: 0 };
        const movesQueue = [];
        function initializePlayer() {
            player.position.x = 0;
            player.position.y = 0;
            player.children[0].position.z = 0;
            position.currentRow = 0;
            position.currentTile = 0;
            movesQueue.length = 0;
        }
        function queueMove(direction) {
            const isValidMove = endsUpInValidPosition(
                { rowIndex: position.currentRow, tileIndex: position.currentTile },
                [...movesQueue, direction]
            );
            if (!isValidMove) return;
            movesQueue.push(direction);
        }
        function stepCompleted() {
            const direction = movesQueue.shift();
            if (direction === "forward") position.currentRow += 1;
            if (direction === "backward") position.currentRow -= 1;
            if (direction === "left") position.currentTile -= 1;
            if (direction === "right") position.currentTile += 1;
            if (position.currentRow > metadata.length - 10) addRows();
            const scoreDOM = document.getElementById("score");
            if (scoreDOM) scoreDOM.innerText = position.currentRow.toString();
        }
        function Renderer() {
            const canvas = document.querySelector("canvas.game");
            if (!canvas) throw new Error("Canvas not found");
            const renderer = new THREE.WebGLRenderer({
                alpha: true,
                antialias: true,
                canvas: canvas,
            });
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.shadowMap.enabled = true;
            return renderer;
        }
        function Road(rowIndex) {
            const road = new THREE.Group();
            road.position.y = rowIndex * tileSize;
            const createSection = (color) =>
                new THREE.Mesh(
                    new THREE.PlaneGeometry(tilesPerRow * tileSize, tileSize),
                    new THREE.MeshLambertMaterial({ color })
                );
            const middle = createSection(0x454a59);
            middle.receiveShadow = true;
            road.add(middle);
            const left = createSection(0x393d49);
            left.position.x = -tilesPerRow * tileSize;
            road.add(left);
            const right = createSection(0x393d49);
            right.position.x = tilesPerRow * tileSize;
            road.add(right);
            return road;
        }
        function Tree(tileIndex, height) {
            const tree = new THREE.Group();
            tree.position.x = tileIndex * tileSize;
            const trunk = new THREE.Mesh(
                new THREE.BoxGeometry(15, 15, 20),
                new THREE.MeshLambertMaterial({ color: 0x4d2926, flatShading: true })
            );
            trunk.position.z = 10;
            tree.add(trunk);
            const crown = new THREE.Mesh(
                new THREE.BoxGeometry(30, 30, height),
                new THREE.MeshLambertMaterial({ color: 0x7aa21d, flatShading: true })
            );
            crown.position.z = height / 2 + 20;
            crown.castShadow = true;
            crown.receiveShadow = true;
            tree.add(crown);
            return tree;
        }
        function Truck(initialTileIndex, direction, color) {
            const truck = new THREE.Group();
            truck.position.x = initialTileIndex * tileSize;
            if (!direction) truck.rotation.z = Math.PI;
            const cargo = new THREE.Mesh(
                new THREE.BoxGeometry(70, 35, 35),
                new THREE.MeshLambertMaterial({ color: 0xb4c6fc, flatShading: true })
            );
            cargo.position.x = -15;
            cargo.position.z = 25;
            cargo.castShadow = true;
            cargo.receiveShadow = true;
            truck.add(cargo);
            const cabin = new THREE.Mesh(new THREE.BoxGeometry(30, 30, 30), [
                new THREE.MeshLambertMaterial({
                    color,
                    flatShading: true,
                    map: truckFrontTexture,
                }),
                new THREE.MeshLambertMaterial({ color, flatShading: true }),
                new THREE.MeshLambertMaterial({
                    color,
                    flatShading: true,
                    map: truckLeftSideTexture,
                }),
                new THREE.MeshLambertMaterial({
                    color,
                    flatShading: true,
                    map: truckRightSideTexture,
                }),
                new THREE.MeshPhongMaterial({ color, flatShading: true }),
                new THREE.MeshPhongMaterial({ color, flatShading: true }),
            ]);
            cabin.position.x = 35;
            cabin.position.z = 20;
            cabin.castShadow = true;
            cabin.receiveShadow = true;
            truck.add(cabin);
            const frontWheel = Wheel(37);
            truck.add(frontWheel);
            const middleWheel = Wheel(5);
            truck.add(middleWheel);
            const backWheel = Wheel(-35);
            truck.add(backWheel);
            return truck;
        }
        function Wheel(x) {
            const wheel = new THREE.Mesh(
                new THREE.BoxGeometry(12, 33, 12),
                new THREE.MeshLambertMaterial({ color: 0x333333, flatShading: true })
            );
            wheel.position.x = x;
            wheel.position.z = 6;
            return wheel;
        }
        function calculateFinalPosition(currentPosition, moves) {
            return moves.reduce((position, direction) => {
                if (direction === "forward")
                    return {
                        rowIndex: position.rowIndex + 1,
                        tileIndex: position.tileIndex,
                    };
                if (direction === "backward")
                    return {
                        rowIndex: position.rowIndex - 1,
                        tileIndex: position.tileIndex,
                    };
                if (direction === "left")
                    return {
                        rowIndex: position.rowIndex,
                        tileIndex: position.tileIndex - 1,
                    };
                if (direction === "right")
                    return {
                        rowIndex: position.rowIndex,
                        tileIndex: position.tileIndex + 1,
                    };
                return position;
            }, currentPosition);
        }
        function endsUpInValidPosition(currentPosition, moves) {
            const finalPosition = calculateFinalPosition(currentPosition, moves);
            if (
                finalPosition.rowIndex === -1 ||
                finalPosition.tileIndex === minTileIndex - 1 ||
                finalPosition.tileIndex === maxTileIndex + 1
            ) {
                return false;
            }
            const finalRow = metadata[finalPosition.rowIndex - 1];
            if (
                finalRow &&
                finalRow.type === "forest" &&
                finalRow.trees.some(
                    (tree) => tree.tileIndex === finalPosition.tileIndex
                )
            ) {
                return false;
            }
            return true;
        }
        function generateRows(amount) {
            const rows = [];
            for (let i = 0; i < amount; i++) {
                const rowData = generateRow();
                rows.push(rowData);
            }
            return rows;
        }
        function generateRow() {
            const type = randomElement(["car", "truck", "forest"]);
            if (type === "car") return generateCarLaneMetadata();
            if (type === "truck") return generateTruckLaneMetadata();
            return generateForesMetadata();
        }
        function randomElement(array) {
            return array[Math.floor(Math.random() * array.length)];
        }
        function generateForesMetadata() {
            const occupiedTiles = new Set();
            const trees = Array.from({ length: 4 }, () => {
                let tileIndex;
                do {
                    tileIndex = THREE.MathUtils.randInt(minTileIndex, maxTileIndex);
                } while (occupiedTiles.has(tileIndex));
                occupiedTiles.add(tileIndex);
                const height = randomElement([20, 45, 60]);
                return { tileIndex, height };
            });
            return { type: "forest", trees };
        }
        function generateCarLaneMetadata() {
            const direction = randomElement([true, false]);
            const speed = randomElement([125, 156, 188]);
            const occupiedTiles = new Set();
            const vehicles = Array.from({ length: 3 }, () => {
                let initialTileIndex;
                do {
                    initialTileIndex = THREE.MathUtils.randInt(
                        minTileIndex,
                        maxTileIndex
                    );
                } while (occupiedTiles.has(initialTileIndex));
                occupiedTiles.add(initialTileIndex - 1);
                occupiedTiles.add(initialTileIndex);
                occupiedTiles.add(initialTileIndex + 1);
                const color = randomElement([0xa52523, 0xbdb638, 0x78b14b]);
                return { initialTileIndex, color };
            });
            return { type: "car", direction, speed, vehicles };
        }
        function generateTruckLaneMetadata() {
            const direction = randomElement([true, false]);
            const speed = randomElement([125, 156, 188]);
            const occupiedTiles = new Set();
            const vehicles = Array.from({ length: 2 }, () => {
                let initialTileIndex;
                do {
                    initialTileIndex = THREE.MathUtils.randInt(
                        minTileIndex,
                        maxTileIndex
                    );
                } while (occupiedTiles.has(initialTileIndex));
                occupiedTiles.add(initialTileIndex - 2);
                occupiedTiles.add(initialTileIndex - 1);
                occupiedTiles.add(initialTileIndex);
                occupiedTiles.add(initialTileIndex + 1);
                occupiedTiles.add(initialTileIndex + 2);
                const color = randomElement([0xa52523, 0xbdb638, 0x78b14b]);
                return { initialTileIndex, color };
            });
            return { type: "truck", direction, speed, vehicles };
        }
        const moveClock = new THREE.Clock(false);
        function animatePlayer() {
            if (!movesQueue.length) return;
            if (!moveClock.running) moveClock.start();
            const stepTime = 0.2;
            const progress = Math.min(1, moveClock.getElapsedTime() / stepTime);
            setPosition(progress);
            setRotation(progress);
            if (progress >= 1) {
                stepCompleted();
                moveClock.stop();
            }
        }
        function setPosition(progress) {
            const startX = position.currentTile * tileSize;
            const startY = position.currentRow * tileSize;
            let endX = startX;
            let endY = startY;
            if (movesQueue[0] === "left") endX -= tileSize;
            if (movesQueue[0] === "right") endX += tileSize;
            if (movesQueue[0] === "forward") endY += tileSize;
            if (movesQueue[0] === "backward") endY -= tileSize;
            player.position.x = THREE.MathUtils.lerp(startX, endX, progress);
            player.position.y = THREE.MathUtils.lerp(startY, endY, progress);
            player.children[0].position.z = Math.sin(progress * Math.PI) * 8;
        }
        function setRotation(progress) {
            let endRotation = 0;
            if (movesQueue[0] == "forward") endRotation = 0;
            if (movesQueue[0] == "left") endRotation = Math.PI / 2;
            if (movesQueue[0] == "right") endRotation = -Math.PI / 2;
            if (movesQueue[0] == "backward") endRotation = Math.PI;
            player.children[0].rotation.z = THREE.MathUtils.lerp(
                player.children[0].rotation.z,
                endRotation,
                progress
            );
        }
        const clock = new THREE.Clock();
        function animateVehicles() {
            const delta = clock.getDelta();
            metadata.forEach((rowData) => {
                if (rowData.type === "car" || rowData.type === "truck") {
                    const beginningOfRow = (minTileIndex - 2) * tileSize;
                    const endOfRow = (maxTileIndex + 2) * tileSize;
                    rowData.vehicles.forEach(({ ref }) => {
                        if (!ref) throw Error("Vehicle reference is missing");
                        if (rowData.direction) {
                            ref.position.x =
                                ref.position.x > endOfRow
                                    ? beginningOfRow
                                    : ref.position.x + rowData.speed * delta;
                        } else {
                            ref.position.x =
                                ref.position.x < beginningOfRow
                                    ? endOfRow
                                    : ref.position.x - rowData.speed * delta;
                        }
                    });
                }
            });
        }
        document
            .getElementById("forward")
            ?.addEventListener("click", () => queueMove("forward"));
        document
            .getElementById("backward")
            ?.addEventListener("click", () => queueMove("backward"));
        document
            .getElementById("left")
            ?.addEventListener("click", () => queueMove("left"));
        document
            .getElementById("right")
            ?.addEventListener("click", () => queueMove("right"));
        window.addEventListener("keydown", (event) => {
            if (event.key === "ArrowUp") {
                event.preventDefault();
                queueMove("forward");
            } else if (event.key === "ArrowDown") {
                event.preventDefault();
                queueMove("backward");
            } else if (event.key === "ArrowLeft") {
                event.preventDefault();
                queueMove("left");
            } else if (event.key === "ArrowRight") {
                event.preventDefault();
                queueMove("right");
            }
        });
        function hitTest() {
            const row = metadata[position.currentRow - 1];
            if (!row) return;
            if (row.type === "car" || row.type === "truck") {
                const playerBoundingBox = new THREE.Box3();
                playerBoundingBox.setFromObject(player);
                row.vehicles.forEach(({ ref }) => {
                    if (!ref) throw Error("Vehicle reference is missing");
                    const vehicleBoundingBox = new THREE.Box3();
                    vehicleBoundingBox.setFromObject(ref);
                    if (playerBoundingBox.intersectsBox(vehicleBoundingBox)) {
                        if (!resultDOM || !finalScoreDOM) return;
                        resultDOM.style.visibility = "visible";
                        finalScoreDOM.innerText = position.currentRow.toString();
                    }
                });
            }
        }
        const scene = new THREE.Scene();
        scene.add(player);
        scene.add(map);
        const ambientLight = new THREE.AmbientLight();
        scene.add(ambientLight);
        const dirLight = DirectionalLight();
        dirLight.target = player;
        player.add(dirLight);
        const camera = Camera();
        player.add(camera);
        const scoreDOM = document.getElementById("score");
        const resultDOM = document.getElementById("result-container");
        const finalScoreDOM = document.getElementById("final-score");
        initializeGame();
        document
            .querySelector("#retry")
            ?.addEventListener("click", initializeGame);
        function initializeGame() {
            initializePlayer();
            initializeMap();
            if (scoreDOM) scoreDOM.innerText = "0";
            if (resultDOM) resultDOM.style.visibility = "hidden";
        }
        const renderer = Renderer();
        renderer.setAnimationLoop(animate);
        function animate() {
            animateVehicles();
            animatePlayer();
            hitTest();
            renderer.render(scene, camera);
        }
    </script>

</body>

</html>